package pnca.analytics;


import pnca.*;
import pnca.Constants;
import MASS.*;                  // MASS Library
import java.util.*;             // Vectors, etc.
import java.util.AbstractMap.*; // for simple key-value pairs
import java.util.logging.Level;

/**
 * @name MonthlyMaxOneDayPrecipitation - Monthly max one day precipitation
 *
 * @author Niko Simonson (Primary)
 * @author Brett Yasutake (Contributing)
 */
public class SimplePrecipitationIndex extends Place {

    // Constants
    // MASS arguments
    private static final String NAME = Constants.ANALYTICSPACKAGE + "SimplePrecipitationIndex";
    private static final String[] MASSARGS = Constants.MASSARGS;    // connection arguments
    private static final int NPROCESSES = Constants.NPROCESSES;     // number of processes to employ
    private static final int NTHREADS = Constants.NTHREADS;         // number of threads to employ
    private static int HANDLE = 0;                            // unique Places handle number
    private static int MAXITERATIONS = Constants.TALLIES;
    // Functions
    private static final int init_ = 0;         // initialize Places
    private static final int readRain_ = 1;    // read temperatures
    private static final int revealDays_ = 2;   // expose days
    private static final int detectDays_ = 3;   // day to index mapping
    private static final int detectMonths_ = 4;  // year to index mapping
    private static final int revealTemps_ = 5;  // expose temperatures, time indices
    private static final int findWetDays_ = 6;    // calculate frost values for one day 
    private static final int recordSimplePrecipitationIndex_ = 7;        // tally frost days for each year
    
    
    private static final String RAIN = Constants.RAINC;  // Temperature variable name
    // Northwest regional climate data sample
    private static final int XRANGE = Constants.XRANGE; // width of data set (# of grid cells)
    private static final int YRANGE = Constants.YRANGE; // length of data set (# of grid cells)

    private static final int METADATALENGTH = Constants.METADATALENGTH; // # of metadata elements
    private static final double FREEZING = Constants.FREEZING;          // freezing temp (K)
    
    // Variables
    private static int fileRange;               // number of computing nodes
    private static Places pacNorthwest = null;  // MASS parallel environment
    private static ProvenanceAdapter pa;        // Stores provenance
    private static String[] fileNames;          // names of selected source files
    
    
    private int myIndex;        // computing node's index
    private double[][] rain;   // temperatures
    private boolean[][] isWet;
    private double[][] maxLows;      // annual tally of frost days
    
    // Date, Year detection
    private int myDay;                          // computing node's day
    private ArrayList<Integer> otherTimes;      // list of indices of later time slices on the same day
    private boolean isFirstDay;                 // this day is distinct from all previous days encountered
    private int myMonth;
    private boolean isFirstTimeOfMonth;
    private ArrayList<Integer> timesOfThisMonth;
    private int myYear;                         // computing node's year
    
    
    /**
     * @name ExtremeFrostDays - 
     * @param interval unused Empty constructor required for MASS Place
     * implementation
     */
    public SimplePrecipitationIndex(Object interval) {
        // A constructor implementation is required for MASS Place
    }

    /**
     * @name callMethod Provides enumerated list of functions to be parallelized
     * in MASS
     *
     * @param funcId - function ID (listed as Constants)
     * @param args - function parameters
     * @return - unused
     */
    public Object callMethod(int funcId, Object args) {
        switch (funcId) {
            case init_:
                return init(args);
            case readRain_:
                return readRain(args);
            case revealDays_:
                return revealDays(args);
            case detectDays_:
                return detectDays(args);
            case detectMonths_:
                return detectMonths(args);
            case revealTemps_:
                return revealTemps(args);
            case findWetDays_:
                return findWetDays(args);  
            case recordSimplePrecipitationIndex_:
                return recordSimplePrecipitationIndex(args);
        }
        return null;
    }

    /**
     * @name init - Initialization information for MASS Places 
     *
     * @param args - unused
     * @return unused
     */
    public Object init(Object args) {
        // Each place keeps track of its time position
        myIndex = index[0];
        
        rain = new double[XRANGE][YRANGE];
        isWet = new boolean[XRANGE][YRANGE];

        return (Object) null;
    }

    /**
     * @name readTemps - Read temperatures from NetCDF file and calculate frosts
     * @param args - Not used
     * @return unused
     */
    public Object readRain(Object args) {
        try {
            // Retreive input values from input adapter
            rain = InputAdapter.read2DFloat(fileNames[myIndex], RAIN, XRANGE, YRANGE);

            // find the day              
            myDay = InputAdapter.getDay(fileNames[myIndex]);
            myMonth = myDay / Constants.MONTHDIVISOR;

        } catch (Exception ex) {
            pa.log(Level.SEVERE, "exception caught in readTemps", ex);
        }

        // Check if values were retreived
        if (rain == null) {
            System.err.println("Null input adapter return");
        }
        return (Object) null;
    }

        
    /**
     * @name revealDays - exchanges each Place's day information
     *
     * @param args - unused
     * @return current day as YYYYMMDD int
     */
    public int revealDays(Object args) {
        return  myDay;
    }
    
    /**
     * @name detectDays - locates all time slices having the same day
     *
     * @param args - unused
     * @return unused
     */
    public Object detectDays(Object args) {
        // for every day before this day
        for (int i = fileRange - 1 - myIndex; i < fileRange - 1; ++i) {
            // if this day is the same as a previous day
            if ((int)inMessages[i] == myDay) {
                // then this is not the first time this day occurred
                isFirstDay = false;
                // don't do anything else
                return null;
            }
        }
        // This is the first occurrence of this day
        isFirstDay = true;
        otherTimes = new ArrayList<Integer>();
        // For all future days,
        for (int j = fileRange; j < inMessages.length - myIndex; ++j) {
            // if there is a future occurrence of this day,
            if ((int)inMessages[j] == myDay) {
                // then record where it exists.
                otherTimes.add(j);
            }
        }
        return null;
    }
    
        
    /**
     * @name detectMonths - Maps months to each index
     * 
     * @param args - unused
     * @return unused
     */
    public Object detectMonths(Object args) {
        // for every time before this time
        for (int i = fileRange - 1 - myIndex; i < fileRange - 1; ++i) {
            // if this month is the same as a previous month,
            if ((int)inMessages[i] / Constants.MONTHDIVISOR == myMonth) {
                // then this is not the first time this month occurred
                isFirstTimeOfMonth = false;
                // don't do anything else
                return null;
            }
        }
        // This is the first occurrence of this year
        isFirstTimeOfMonth = true;
        timesOfThisMonth = new ArrayList<Integer>();
        // For all future times,
        for (int j = fileRange - 1; j < inMessages.length - myIndex; ++j) {
            // if there is a future occurrence of this month,
            if ((int)inMessages[j] / Constants.MONTHDIVISOR == myMonth) {
                // then record where it exists.
                timesOfThisMonth.add(j);
            }
        }
        return null;
    }
    
    /**
     * @name revealTemps - exchanges frost days and times with neighbors
     *
     * @param args - unused
     * @return frost table showing whether each location was below freezing
     *         time identity
     */
    public Object revealTemps(Object args) {
        Object[] container = new Object[4];
        
        container[0] = isFirstDay;
        container[1] = rain;
        container[2] = isFirstTimeOfMonth;
        container[3] = timesOfThisMonth;
        
        return container;
    }
    
    /**
     * @name findLow locate the low temperature of the day
     *
     * @param args - unused
     * @return unused
     */
    public Object findWetDays(Object args) {
        // if this is the first occurence of a day
        if (isFirstDay) {
            double[][] nextTime;          // frost table to be checked
            // and if there are future occurences of this day
            if (otherTimes != null) {
                // for every future occurence
                for (int i = 0; i < otherTimes.size(); ++i) {
                    // find that occurence's table of frosts
                    Object[] container = (Object[]) inMessages[otherTimes.get(i)];
                    nextTime = (double[][]) container[1];

                    // for every element in the table of precipitation
                    for (int j = 0; j < XRANGE; ++j) {
                        for (int k = 0; k < YRANGE; ++k) {
                                rain[j][k] += nextTime[j][k];
                                
                                if(rain[j][k] > 1.0) {
                                    isWet[j][k] = true;
                                }
                                else {
                                    isWet[j][k] = false;
                                }
  
                        } // next k
                    } // next j
                } // next i
            } // end if otehrTimes not null
        } // end if is first day
        return null;
    }
    
    /**
     * @name recordMaxLows - find max lows for each month and record the results
     *
     * @param args - unused
     * @return unused
     */
    public Object recordSimplePrecipitationIndex(Object args) {
        // extract time identifier
        Object[] container;
        container = (Object[]) inMessages[fileRange - 1];
        double[][] target;
        // if this is the first time of a month
        if ((boolean) container[2]) {
            try {
                
                int[][] counter = new int[XRANGE][YRANGE];
                for (int a = 0; a < XRANGE; ++a) {
                    for (int b = 0; b < YRANGE; ++b) {
                        counter[a][b] = 0;
                        rain[a][b] = 0.0;
                    }
                }
     
                // extract the list of times of this month
                ArrayList<Integer> monthTimes = (ArrayList<Integer>) container[3];

                // for every time associated with this month
                for (int i = 0; i < monthTimes.size(); ++i) {
                    //extrac the day identifier
                    container = (Object[]) inMessages[monthTimes.get(i)];
                    // if this is the first time in a day
                    if ((boolean) container[0]) {
                        // obtain its table of frost locations
                        target = (double[][]) container[1];
                        // for each location in the table
                        for (int j = 0; j < XRANGE; ++j) {
                            for (int k = 0; k < YRANGE; ++k) {
                                if (isWet[j][k]) {
                                    counter[j][k]++;
                                    rain[j][k] += target[j][k];
                                } // end if is wet
                            } // next k
                        } // next j
                    } // end if is distinct day
                } // next month time slice
                for (int j = 0; j < XRANGE; ++j) {
                    for (int k = 0; k < YRANGE; ++k) {
                        if (counter[j][k] > 0) rain[j][k] /= counter[j][k];
                        else rain[j][k] = 0.0;
                    }
                }
                // Write tally for this month to results file
                Integer[] myRanges = new Integer[2];    // two dimensions
                myRanges[0] = XRANGE;                   // west-east
                myRanges[1] = YRANGE;                   // south-north
                // create entry for day tally
                SimpleEntry<String, Integer[]> myVar = new SimpleEntry("SimplePrecipitationIndex", myRanges);
                // create results adapter to store tally
                ResultsAdapter frostTally = new ResultsAdapter(ResultsAdapter.INT, "SimplePrecipitationIndex" + myMonth, fileNames[myIndex], myVar);
                // log the tally
                frostTally.log2DDouble("SimplePrecipitationIndex", rain);
            } // end if first time in a month
            catch (Exception ex) {
                pa.log(Level.SEVERE, "Exception caught in recordSimplePrecipitationIndex", ex);
            }
            
        } // end if first time in a month
        return null;
    }
    

    public static int getMaxIterations() {
        return MAXITERATIONS;
    }
    
     /**
     * @name setFiles - Set file name array to store file names
     * @param fileArray - Array containing output result file names
     */
    public static void setFiles(String[] fileArray) {
        fileNames = fileArray;
        fileRange = fileArray.length;
    }
    
    
    /**
     * @name initialize - Initialize MASS, Places objects, and other data
     * structured
     * @param status - Status adapter associated with this analytics module
     */
    public static void initialize(StatusAdapter status, int handle) {
        try {
            // Dummy object required by MASS
            Object[] fixer = new Object[fileRange];
            HANDLE = handle;

            // Provenance collection
            status.reportMessage("Storing Metadata");
            SimpleEntry<String, String>[] provenanceEntries = collectProvenance();
            pa = new ProvenanceAdapter("SimplePrecipitationIndexProvenance", fileNames[0]);
            pa.create(provenanceEntries);

            // Start MASS
            status.reportMessage("Starting MASS");
            MASS.init(MASSARGS, NPROCESSES, NTHREADS);

            // Initialize parallel computing space (Places object)
            pacNorthwest = new Places(handle, NAME, null,
                    fileRange);
            status.reportMessage("Computational Nodes created");

            // Initialize all computing nodes
            pacNorthwest.callAll(init_, fixer);
            status.reportMessage("Computational Nodes initialized");
        } catch (Exception ex) {
            pa.log(Level.SEVERE, null, ex);
        }
    }

    /**
     * @name runAnalysis Performs entire analysis, manages MASS
     *
     * @param status StatusModel UI view
     * @throws Exception
     */
    public static long runAnalysis(StatusAdapter status) throws Exception {
        long start, end; // Data provenance for execution time
        long totalTime = -1;

        // Dummy object to prevent array over- or underflow
        Object[] fixer = new Object[fileRange];

        // all computing nodes visible to each other, but with overflow and underflow
        Vector<int[]> neighbors = new Vector();

        // We always need it to reference a completely new array.
        for (int i = 1 - fileRange; i < fileRange; ++i) {
            int[] destination = {i};
            neighbors.add(destination);
        }
        
        try {
            // Begin time measurement
            start = new Date().getTime();

            // Read temps at each time slice
            pacNorthwest.callAll(readRain_, fixer);
            status.reportMessage("Source values read");

            // Detect distinct days and years
            pacNorthwest.exchangeAll(HANDLE, revealDays_, neighbors);
            status.reportMessage("Performing day detection");
            pacNorthwest.callAll(detectDays_, fixer);
            pacNorthwest.callAll(detectMonths_, fixer);
            
            // Calculate days of frost
            pacNorthwest.exchangeAll(HANDLE, revealTemps_, neighbors);
            pacNorthwest.callAll(findWetDays_, fixer);                        
            status.reportMessage("Daily rain calculated");
            
            // Tally days of frost     
            pacNorthwest.exchangeAll(HANDLE, revealTemps_, neighbors);
            pacNorthwest.callAll(recordSimplePrecipitationIndex_, fixer);
            status.reportMessage("Calculations completed");

            // End time measurement
            end = new Date().getTime();
            totalTime = end - start;    //milliseconds
            status.reportMessage("Results stored");

            // Report execution time
            System.err.println("Segment execution time (milliseconds): "
                    + totalTime);
            status.reportMessage("Segment execution time (milliseconds): "
                    + totalTime);
        } catch (Exception e) {
            e.printStackTrace();
        }
        finish();
        return totalTime;
    }

    /**
     * @name collectProvenance - Returns a SimpleEntry array holding provenance
     * data
     * @return SimpleEntry - Holds provenance key-value pairs
     */
    private static SimpleEntry<String, String>[] collectProvenance() {
        SimpleEntry<String, String>[] provenanceEntries = new SimpleEntry[METADATALENGTH];
        provenanceEntries[0] = new SimpleEntry<>("P_Controlling_Node", MASSARGS[0]);
        provenanceEntries[1] = new SimpleEntry<>("P_Controlling_Node_Password", MASSARGS[1]);
        provenanceEntries[2] = new SimpleEntry<>("P_Machine_Life_File_Name", MASSARGS[2]);
        provenanceEntries[3] = new SimpleEntry<>("P_Port_Number", MASSARGS[3]);
        provenanceEntries[4] = new SimpleEntry<>("P_No_of_Processes", String.valueOf(NPROCESSES));
        provenanceEntries[5] = new SimpleEntry<>("P_No_of_Threads", String.valueOf(NTHREADS));
        provenanceEntries[6] = new SimpleEntry<>("P_No_of_Nodes", String.valueOf(fileRange));

        return provenanceEntries;
    }

    /**
     * @name finish - Terminate and clean-up MASS by calling MASS's finish
     * function
     */
    public static void finish() {
        MASS.finish();
    }
}
